/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 *    BinC45ModelSelection.java
 *    Copyright (C) 1999-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.classifiers.trees.j48;

import java.util.Enumeration;

import weka.core.Attribute;
import weka.core.Instances;
import weka.core.Utils;

/**
 * Class for selecting a C4.5-like binary (!) split for a given dataset.
 * 
 * @author Eibe Frank (eibe@cs.waikato.ac.nz)
 * @version $Revision$
 */
public class BinC45ModelSelection extends ModelSelection {

    /** for serialization */
    private static final long serialVersionUID = 179170923545122001L;

    /** Minimum number of instances in interval. */
    protected final int m_minNoObj;

    /** Use MDL correction? */
    protected final boolean m_useMDLcorrection;

    /** The FULL training dataset. */
    protected Instances m_allData;

    /** Do not relocate split point to actual data value */
    protected final boolean m_doNotMakeSplitPointActualValue;

    /**
     * Initializes the split selection method with the given parameters.
     * 
     * @param minNoObj         minimum number of instances that have to occur in at
     *                         least two subsets induced by split
     * @param allData          FULL training dataset (necessary for selection of
     *                         split points).
     * @param useMDLcorrection whether to use MDL adjustement when finding splits on
     *                         numeric attributes
     */
    public BinC45ModelSelection(int minNoObj, Instances allData, boolean useMDLcorrection, boolean doNotMakeSplitPointActualValue) {
        m_minNoObj = minNoObj;
        m_allData = allData;
        m_useMDLcorrection = useMDLcorrection;
        m_doNotMakeSplitPointActualValue = doNotMakeSplitPointActualValue;
    }

    /**
     * Sets reference to training data to null.
     */
    public void cleanup() {

        m_allData = null;
    }

    /**
     * Selects C4.5-type split for the given dataset.
     */
    @Override
    public final ClassifierSplitModel selectModel(Instances data) {

        double minResult;
        BinC45Split[] currentModel;
        BinC45Split bestModel = null;
        NoSplit noSplitModel = null;
        double averageInfoGain = 0;
        int validModels = 0;
        boolean multiVal = true;
        Distribution checkDistribution;
        double sumOfWeights;
        int i;

        try {

            // Check if all Instances belong to one class or if not
            // enough Instances to split.
            checkDistribution = new Distribution(data);
            noSplitModel = new NoSplit(checkDistribution);
            if (Utils.sm(checkDistribution.total(), 2 * m_minNoObj) || Utils.eq(checkDistribution.total(), checkDistribution.perClass(checkDistribution.maxClass()))) {
                return noSplitModel;
            }

            // Check if all attributes are nominal and have a
            // lot of values.
            Enumeration<Attribute> enu = data.enumerateAttributes();
            while (enu.hasMoreElements()) {
                Attribute attribute = enu.nextElement();
                if ((attribute.isNumeric()) || (Utils.sm(attribute.numValues(), (0.3 * m_allData.numInstances())))) {
                    multiVal = false;
                    break;
                }
            }
            currentModel = new BinC45Split[data.numAttributes()];
            sumOfWeights = data.sumOfWeights();

            // For each attribute.
            for (i = 0; i < data.numAttributes(); i++) {

                // Apart from class attribute.
                if (i != (data).classIndex()) {

                    // Get models for current attribute.
                    currentModel[i] = new BinC45Split(i, m_minNoObj, sumOfWeights, m_useMDLcorrection);
                    currentModel[i].buildClassifier(data);

                    // Check if useful split for current attribute
                    // exists and check for enumerated attributes with
                    // a lot of values.
                    if (currentModel[i].checkModel()) {
                        if ((data.attribute(i).isNumeric()) || (multiVal || Utils.sm(data.attribute(i).numValues(), (0.3 * m_allData.numInstances())))) {
                            averageInfoGain = averageInfoGain + currentModel[i].infoGain();
                            validModels++;
                        }
                    }
                } else {
                    currentModel[i] = null;
                }
            }

            // Check if any useful split was found.
            if (validModels == 0) {
                return noSplitModel;
            }
            averageInfoGain = averageInfoGain / validModels;

            // Find "best" attribute to split on.
            minResult = 0;
            for (i = 0; i < data.numAttributes(); i++) {
                if ((i != (data).classIndex()) && (currentModel[i].checkModel())) {
                    // Use 1E-3 here to get a closer approximation to the original
                    // implementation.
                    if ((currentModel[i].infoGain() >= (averageInfoGain - 1E-3)) && Utils.gr(currentModel[i].gainRatio(), minResult)) {
                        bestModel = currentModel[i];
                        minResult = currentModel[i].gainRatio();
                    }
                }
            }

            // Check if useful split was found.
            if (Utils.eq(minResult, 0)) {
                return noSplitModel;
            }

            // Add all Instances with unknown values for the corresponding
            // attribute to the distribution for the model, so that
            // the complete distribution is stored with the model.
            bestModel.distribution().addInstWithUnknown(data, bestModel.attIndex());

            // Set the split point analogue to C45 if attribute numeric.
            if (!m_doNotMakeSplitPointActualValue) {
                bestModel.setSplitPoint(m_allData);
            }
            return bestModel;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Selects C4.5-type split for the given dataset.
     */
    @Override
    public final ClassifierSplitModel selectModel(Instances train, Instances test) {

        return selectModel(train);
    }

}
